/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

 /***************************************************************************
 * Name: ngl_client_uds.c
 *
 * Purpose: implementation ngl client with unix domain socket
 *
 * Developer:
 *   wen.gu , 2023-04-28
 *
 * TODO:
 *
 ***************************************************************************/
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/fcntl.h>
#include <pthread.h>
#include <errno.h>
#include <sys/select.h>

#include "ngl/client/ngl_client_uds.h"
#include "ngl/ngl_message_parser2.h"


#define UDS_CLIENT_SELECT_TIMEOUT_MS 40

#define UDS_CLIENT_RECV_BUF_SIZE (1024 * 10)


typedef struct _uds_client_s uds_client_t;

struct _uds_client_s {
    ngl_client_t supper;
    const char* url;
    int socket_fd;
    pthread_t on_receive_worker;
    ngl_bool_t is_running;
    ngl_message_parser2_t parser;
} ;

/***************************************************************************
 * inner function  implementation
 ***************************************************************************/

static const char* uds_url_dup(const char* url) {
    uint32_t len = strlen(url);
    char* dup_url = (const char*)malloc(len + 1);

    if (dup_url) {
        memcpy(dup_url, url, len);
        dup_url[len] = '\0';
    }

    return dup_url;
}


static ngl_error_t ngl_socket_set_nonblock_and_linger(int sockfd) {
    int status;
    struct linger l_opt;

    status = fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
    if (status == -1) {
        printf("Socket cannot be changed to NON BLOCK\n");
        return NGL_ErrInvalidStatus;
    }

    l_opt.l_onoff = 1;
    l_opt.l_linger = 10;

    if (setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l_opt, sizeof l_opt) < 0) {
        printf("Failed to set socket linger option\n");
    }

    return NGL_ErrOK;
}



static void* uds_client_receive_worker(void* opaque) {
    uds_client_t* client = (uds_client_t*)opaque;
    fd_set read_set;
    int32_t socket_id = client->socket_fd;
    ngl_client_t* supper = &client->supper;

    struct timeval select_timeout = {0};  //timeout time   
    select_timeout.tv_sec = 0;   /** */
    select_timeout.tv_usec = UDS_CLIENT_SELECT_TIMEOUT_MS * 1000;
    FD_ZERO(&read_set);    
    FD_SET(socket_id, &read_set);

    uint8_t* recv_buf = (uint8_t*)malloc(UDS_CLIENT_RECV_BUF_SIZE);

    if (!recv_buf) {
        return NULL;
    }
    
    while (client->is_running == NGL_TRUE) {
        int32_t ret = select(socket_id + 1, &read_set, NULL, NULL, &select_timeout);

        if ((ret > 0) && FD_ISSET(socket_id, &read_set)) {
            int32_t recev_size = read(socket_id, recv_buf, UDS_CLIENT_RECV_BUF_SIZE);

            if (recev_size > 0) {
                uint8_t* end_ptr = recv_buf + recev_size;
                uint8_t* data_ptr = recv_buf;

                while (data_ptr < end_ptr) {
                    ngl_message_t* msg = NULL;
                    data_ptr = ngl_msg_parser2_fill_data(&client->parser, &msg, data_ptr, end_ptr);

                    if (msg) {
                        if (supper->on_receive) {
                            supper->on_receive(supper->user_opaque, msg);
                        } else {
                            ngl_message_destroy(msg);
                        }
                    }
                }
            }
        }
    }

    if (recv_buf) {
        free(recv_buf);
    }
}


static ngl_error_t uds_client_connect(ngl_client_t* self) {
    if (!self) {
        return NGL_ErrInvalidArgument;
    }
    struct sockaddr_un remote;
    uds_client_t* client = (uds_client_t*)self;
    remote.sun_family = AF_UNIX;
    strncpy(remote.sun_path, client->url, sizeof(remote.sun_path));

    if (connect(client->socket_fd, (struct sockaddr *)&remote, sizeof(remote)) == -1) {
        printf("Socket %s cannot be opened (errno=%d). Retrying later...\n",  client->url, errno);
        return NGL_ErrUndefined;
    }

    pthread_attr_t type;

    /* Set the thread attributes */
    if (pthread_attr_init(&type) != 0) {
        printf("Couldn't initialize pthread attributes");
        return NGL_ErrUndefined;
    }

    pthread_attr_setdetachstate(&type, PTHREAD_CREATE_JOINABLE);
    client->is_running = NGL_TRUE;

    int32_t ret = pthread_create(&client->on_receive_worker, &type, uds_client_receive_worker, client);
    if (ret == -1) {
        return NGL_ErrUndefined;
    }

    return NGL_ErrOK;
}

static ngl_error_t uds_client_disconnect(ngl_client_t* self) {
    if (!self) {
        return NGL_ErrInvalidArgument;
    }

    uds_client_t* client = (uds_client_t*)self;

    if (client->on_receive_worker) {
        client->is_running = NGL_FALSE;
        pthread_join(client->on_receive_worker, NULL);
        client->on_receive_worker = NULL;
    }

    if (client->socket_fd != -1) {
        //closesocket(client->socket_fd);
        shutdown(client->socket_fd, SHUT_RDWR);
        client->socket_fd = -1;
    }

    return NGL_ErrOK;
}

/** if  opaque is NGL_UDS_CLIENT_MSG_BROADCAST_OPAQUE, then indicate broad cast current message */
static ngl_error_t uds_client_send(ngl_client_t* self, ngl_message_t* msg) {
    if (!self || !msg) {
        return NGL_ErrInvalidArgument;
    }

    uds_client_t* client = (uds_client_t*)self;
    int32_t msg_size = 0;
    const uint8_t* msg_buf = ngl_message_serialized_buffer(msg, (uint16_t*)&msg_size);

    if (!msg_buf || (msg_size == 0)) {
        return NGL_ErrInsufficientResources;
    }

    int32_t send_size = 0;
    do
    {
        int32_t ret = ngl_transport_write(client->socket_fd, msg_buf, (msg_size - send_size));
        if (ret < 0) {
            return NGL_ErrUndefined;
        }
        send_size += ret;
    } while (send_size < msg_size);

    return NGL_ErrOK;

}

static void uds_client_free_fn(ngl_client_t* self) {
    if (self) {
        uds_client_t* client = (uds_client_t*)self;
        uds_client_disconnect(self);
        ngl_client_free_method(self);
    }
}

/***************************************************************************
 * API function  implementation
 ***************************************************************************/


ngl_client_t* ngl_client_uds_new(const char* url, ngl_client_on_receive on_receive, void* user_opaque) {
    if (!url || !on_receive) {
        return NULL;
    }

    uds_client_t* client = (uds_client_t*)malloc(sizeof(uds_client_t));
    if (!client) {
        return NULL;
    }

    memset(client, 0, sizeof(uds_client_t));

    int sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sockfd == -1) {
        free(client);
        return NULL;
    }

    if (ngl_socket_set_nonblock_and_linger(sockfd) != NGL_ErrOK) {
        free(client);
        return NULL;
    }

    client->url = uds_url_dup(url);

    if (!client->url) {
        close(sockfd);
        free(client);
        return NULL;
    }

    ngl_client_t* supper = &client->supper;
    ngl_client_init_instance(supper, on_receive, user_opaque);
    supper->connect = uds_client_connect;
    supper->disconnect = uds_client_disconnect;
    supper->send = uds_client_send;
    supper->free_fn = uds_client_free_fn;
    client->socket_fd = sockfd;

    return (ngl_client_t*)client;
}
